
*******************************************************************************
**
**  MC Bouncy Ball for the TRS-80 MC-10.
**  Based on Lee Patterson's Bouncy Ball for the Tandy Color Computer.
**
*******************************************************************************



*****  Convenience Macros
            .macro      pshd                    ; push accumulator D onto stack
            pshb
            psha
            .endm

            .macro      puld                    ; pull from stack into accumulator D
            pula
            pulb
            .endm


*****  MC6803 Internal Registers
TCSR        equ         $0008                   ; Timer Control and Status Register
TCNT        equ         $0009                   ; Timer free-running Counter
TOCR        equ         $000B                   ; Timer Output Compare Register
RAMCR       equ         $0014                   ; RAM Control Register


*****  MC-10 ROM Vectors and Hardware Addressing
KEYTABLE    equ         $4231                   ; keyboard roll-over table
KEYVEC      equ         $FFDC                   ; vector to the KEYIN subroutine
VDGSND      equ         $BFFF                   ; VDG control / Sound Output
DPRAM       equ         $0080                   ; start of Direct Page RAM


*****  Instruction Opcodes
SKP1        equ         $21                     ; used for skipping one-byte instuctions  ( brn  nn    )
SKP2        equ         $8C                     ; used for skipping two-byte instuctions  ( cpx  #nnnn )


*****  Timing Interval
kTimingNTSC equ         14934                   ; number of CPU cycles per 60 Hz NTSC field


*****  State values for the Game Play Loop
kRetryState equ         -1                      ; re-start level from beginning
kIdleState  equ         0                       ; ask player to select a level
kRunState   equ         1                       ; level play in progress
kDeadState  equ         2                       ; end level due to death
kDoneState  equ         3                       ; end level due to completion

*****  Level Map Info
kNumLevels  equ         12                      ; number of embedded levels (not including title graphics)
kMapWidth   equ         450                     ; width of level map in bytes (MUST BE EVEN)
kMapHeight  equ         15                      ; height of the level map in rows
kMapSize    equ         kMapWidth*kMapHeight    ; total size of the level map
kMinScroll  equ         16                      ; minimum scroll position (screen center)
kTimeMult   equ         100                     ; time units multiplier


*****  Tile values contained in the expanded Level Map (SG4 graphics data)
kOpenSpace  equ         $80                     ; open space for the ball to move
kScoreTile  equ         $8F                     ; hit these tiles to score points
kSlowTile   equ         $9F                     ; wall tile which temporarily reduces ball speed
kWaterTrap  equ         $AF                     ; water hazard (only present in water levels)
kDeathTrap  equ         $BF                     ; immediate death trap (only present in non-water levels)
kSolidWall  equ         $CF                     ; permanent walls
kHiddenWall equ         $D0                     ; invisible wall to prevent horizontal movement
kOffWorld   equ         $DF                     ; indicates out-of-bounds (never present in the map)
kExitTile   equ         $EF                     ; goal location to complete the level
kBreakAway  equ         $FF                     ; non-score tiles removed from the map when you bounce off them


*****  Control values for the Print sub-routine
kPadZero    equ         $81                     ; pad numeric parameter with leading zeroes
kPadSpace   equ         $80                     ; pad numeric parameter with leading spaces
kCentered   equ         $40                     ; center text on specified line number
kCurrentPos equ         $00                     ; print at the current cursor position


*****  Settings
kMaxLives   equ         3                       ; number of lives per level


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
            .data

* Level Record
            org         0
lev.name    rmb         15                      ; level name (padded with 0x80)
lev.width   rmb         2                       ; level width in tiles
lev.min     rmb         1                       ; minimum number of score tiles required to complete level
lev.total   rmb         1                       ; total number of score tiles contained in the level
lev.time    rmb         1                       ; time units allowed for the level (multiplied by kTimeMult)
lev.water   rmb         1                       ; 0 = Death Traps,  1 = Water Hazards
lev.sizeof  equ         *


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Video Page Record
            org         0
pag.buffer  rmb         2                       ; address of the 540 byte buffer
pag.queued  rmb         1                       ; non-zero when queued for display
pag.hStart  rmb         1                       ; byte offset for 1/4 tile scroll position
pag.sizeof  equ         *


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Direct Page Global Variables
            org         DPRAM
gTempPtr    rmb         2                       ; temporary storage for indexing pointers
gTfrTemp    rmb         2                       ; temporary storage for register transfers
gCounter1   rmb         1                       ; multi-purpose loop counter
gCounter2   rmb         1                       ; multi-purpose loop counter
gCursorPtr  rmb         2                       ; cursor address for the print routine (points into page buffer 0)
gCheat      rmb         1                       ; non-zero enables cheat mode
gObitStr    rmb         2                       ; pointer to obituary string
gLevelNum   rmb         1                       ; index of current level (0..kNumLevels-1)
gLevelTime  rmb         2                       ; expanded time value for the current level
gTimeLeft   rmb         2                       ; down-counting level timer during play
gLevFlags   rmb         kNumLevels              ; array of booleans indicating which levels have been completed
gPixAlign   rmb         1                       ; non-zero if ball pixel is right-aligned in its cell
gScrollPos  rmb         2                       ; horizontal position in map of cell containing the ball (16..)
gBallPos    rmb         1                       ; \ vertical position of ball in pixels (0..29)
gBallDir    rmb         1                       ; / current vertical direction of travel for the ball (-1 = up, 1 = down)
gBallPace   rmb         1                       ; \ timing control for ball movement
gBallSpeed  rmb         1                       ; /\ ball speed (1 = normal, 9 = slow)
gSlowTime   rmb         1                       ;  / remaining times the ball will move in slow speed before returning to normal
gCurrTile   rmb         2                       ; pointer to tile in the level map where ball is positioned
gGameState  rmb         1                       ; current state of the play loop (see constants above)
gGameOver   rmb         1                       ; non-zero when current game has concluded (user quit or no more lives)
gMinHits    rmb         1                       ; minimum number of score tiles required to complete current level
gMaxHits    rmb         1                       ; number of score tiles initially present in the current level
gHits       rmb         1                       ; number of score tiles hit
gHitsShown  rmb         1                       ; number of score hits currently indicated by the status line
gNumLives   rmb         1                       ; number of lives remaining
gGameScore  rmb         2                       ; running score total for all levels of a game
gHighScore  rmb         2                       ; high score for the current program session
gTallyDiv   rmb         2                       ; \
gTallyInc   rmb         1                       ; /
gPreviewMax rmb         2                       ; 
gSoundTimer rmb         2                       ; 
gVidTiming  rmb         2                       ; video timing (NTSC or PAL)
gTimer      rmb         2                       ; incremented on every tick by the IRQ service routine
gTimer8     rmb         2                       ; incremented once every 8 ticks
gNeedRender rmb         1                       ; flag set for frames that need to render the map (scroll or ball movement)
gScrollPace rmb         1                       ; timing control for scrolling
gInertia    rmb         1                       ; scrolling intertia flag (throttles first scroll following key-down)
gRenderPage rmb         1                       ; \
gBlitPage   rmb         1                       ; /
gPage0      rmb         pag.sizeof              ; \
gPage1      rmb         pag.sizeof              ; /

** Number to String conversion
numStrBuf   rmb         5                       ; 5 character output buffer
numStrVal   rmb         2                       ; value used for calculation
numStrPos   rmb         1                       ; current char position in numStrBuf
numStrSig   rmb         1                       ; number of significant digits produced
numStrZer   rmb         1                       ; flag used for leading-zeroes indicator
numStrDiv   rmb         2                       ; current power of ten divisor

dpSize      equ         $0100-DPRAM


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* More System RAM
*
            org         $4000

VIDRAM      rmb         512                     ; video RAM
STATLINE    equ         *-32                    ; last line of the display
SCIVEC      rmb         3                       ; SCI
TOFVEC      rmb         3                       ; Timer Overflow
OCFVEC      rmb         3                       ; Output Compare
ICFVEC      rmb         3                       ; Input Capture
IRQVEC      rmb         3                       ; IRQ1
SWIVEC      rmb         3                       ; SWI


            org         $42AF                   ; end of system RAM Hooks
gPageBuf0   rmb         540                     ; buffer for video page 0
gPageBuf1   rmb         540                     ; buffer for video page 1
STAKLIMIT   rmb         1                       ; stack must not grow below this point 


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
            .code
            org         $4600                   ; Load and EXEC address


* Program entry point
            ldx         #DPRAM                  ; initialize Direct Page RAM
            ldab        #dpSize
            jsr         MemZero
            ldd         #gPageBuf0              ; setup page buffers
            std         gPage0+pag.buffer
            ldd         #gPageBuf1
            std         gPage1+pag.buffer


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Video Synchronization Screen
*
            jsr         ClrScreen               ; clear page 0 buffer to black
            ldd         #$040A                  ; set cursor position for..
            jsr         Locate                  ; ..printing the box
            bsr         vsync010

* Five lines of source data for box graphics.
* Each line consists of: left, fill1, fill2, right

            fcb         $C1,$C2,$C3,$C3
            fcb         $C5,$CA,$8F,$60
            fcb         $C5,$CA,$60,$8F
            fcb         $C5,$CA,$8F,$60
            fcb         $C4,$C8,$CC,$CC

printBox4   bsr         *+2
            bsr         *+2
printBox1   pshx                                ; save source line ptr on the stack
            ldd         0,x                     ; data for left and right sides
            ldx         2,x                     ; data for interior
            pshx                                ; save interior data on the stack
            ldx         gCursorPtr              ; destination ptr for current line
            staa        0,x                     ; put byte for left side
            stab        23,x                    ; put byte for right side
            ldab        #11                     ; setup fill counter for..
            stab        gCounter1               ; ..11 words (22 bytes)
            puld                                ; pull interior data from stack
boxRpt      std         1,x                     ; put one word of interior fill
            inx                                 ; increment destination ptr
            inx
            dec         gCounter1               ; decrement fill counter
            bne         boxRpt                  ; loop until filled
            ldab        #32-22                  ; advance destination..
            abx                                 ; ..ptr to next line
            stx         gCursorPtr              ; update cursor position
            pulx                                ; pull source line ptr from stack
            ldab        #4                      ; advance source ptr to..
            abx                                 ; ..data for next line
            rts                                 ; return
            
vsync010    pulx                                ; pull ptr to source data
            bsr         printBox4               ; render 1st 4 lines of the box
            bsr         printBox1               ; render 5th line of the box
            jsr         Print                   ; print the text lines...
            fcb         8,0
            fcz         "SYNCHRONIZE VIDEO"
            jsr         Print
            fcb         2,3
            fcz         "PRESS SPACEBAR UNTIL BOX BELOW"
            jsr         Print
            fcb         2,4
            fcz         "IS FILLED WITH A CHECKERBOARD."
            jsr         Print
            fcb         4,7
            fcz         "PRESS ENTER KEY WHEN READY"

            sei
            ldd         #kTimingNTSC            ; set timing..
            std         gVidTiming              ; ..for NTSC video
            clr         TCSR                    ; clear any pending timer IRQ
            ldd         #8000                   ; setup a first firing time for TOC
            addd        TCNT
            std         TOCR
            ldaa        $F956                   ; check for Alice 4K ROM
            cmpa        #$40                    ; skip video synch if..
            bne         IrqSetup                ; ..running on an Alice
            ldd         #$4000                  ; values to enable/disable on-chip CPU RAM
            stab        RAMCR                   ; disable on-chip RAM
            ldx         #'DA'                   ; store the test pattern into a..
            stx         gTempPtr                ; ..location covered by the on-chip RAM
            staa        RAMCR                   ; enable on-chip RAM
            cpx         gTempPtr                ; check for test pattern
            beq         IrqSetup                ; skip video synch if running on emulator
            jsr         ScreenFlush             ; flush page buffer to video RAM

vsyncLoop   ldd         gVidTiming              ; get video frame timing
frameAdjust std         gVidTiming              ; store new frame timing (when changed)
loopAdjust  std         gTempPtr                ; store timing just for this frame
            ldaa        #$40                    ; OCF/CSS mask
            ldab        #147                    ; loop count for hilite range
waitOCF     bita        TCSR                    ; is OCF flag set?
            beq         waitOCF                 ; loop if no
            staa        VDGSND                  ; change color set to 1
hiliteBand  ldaa        #1          \           ; \ 12 cycle
            mul                      | 17       ; /  delay
            decb                     |          ; decrement loop counter
            bne         hiliteBand  /           ; loop for 2499 cycles or 44 scan lines (17 * 147 / 57)
            stab        VDGSND                  ; revert color set back to 0
            ldd         gTempPtr                ; get normal or adjusted cycle count for this frame
            addd        TOCR                    ; set Output Compare register..
            std         TOCR                    ; ..for next frame sync time

            jsr         GetKey                  ; read keyboard
            cmpa        #$0D                    ; if ENTER key pressed then..
            beq         IrqSetup                ; ..go setup the timing interrupt
            ldaa        KEYTABLE+7              ; read keyboard rollover table for column with spacebar
            cmpa        #$F7                    ; is the spacebar currently pressed?
            bne         vsyncLoop               ; loop if no
            ldd         gVidTiming              ; adjust timing for the next..
            addd        #57*2                   ; ..field to move sync position..
            bra         loopAdjust              ; ..down by 2 scan lines

IrqSetup    ldx         #ServiceTOC             ; install secondary RAM vector..
            stx         OCFVEC+1                ; ..for the TOC interrupt
            ldaa        #$7E
            staa        OCFVEC
            ldaa        #$08                    ; enable the TOC interrupt
            staa        TCSR
            lds         #*                      ; new stack located below remaining code
            cli                                 ; clear interrupt mask


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Attract Mode
*
Attract     jsr         ClrScreen               ; clear page 0 buffer to black
            jsr         ScreenFlush             ; display the cleared screen
            ldab        #-1
            stab        gLevelNum
            jsr         PrepLevel               ; prepare level map with title graphics
            jsr         Delay12
            jsr         CurrentLev              ; get level data for title graphics
            ldd         lev.width,x             ; get width of the map
            asld                                ; multiply by 4 (tile width)
            asld
            std         gPreviewMax             ; set max scroll for preview
            ldd         #kMinScroll             ; set current scroll postion..
            std         gScrollPos              ; ..to minimum value (far left)

ttlFrame    jsr         RenderMap               ; render current frame into page 0
            jsr         GetKey                  ; read the keyboard
            beq         ttlScroll               ; branch if no key pressed
            jmp         NewGame                 ; start a new game


ttlScroll   inc         pag.queued,x            ; queue page 0 for display
            jsr         WaitBlit                ; wait for ISR to blit the page
            jsr         DelayOne                ; delay one tick
            ldx         gScrollPos              ; bump the scroll position
            inx
            stx         gScrollPos
            cpx         gPreviewMax             ; have we reached the far right side?
            blo         ttlFrame                ; loop if no
            jsr         Delay12                 ; delay 12 ticks

* Display High Score / Instructions screen
            jsr         ClrScreen
            jsr         Print
            fcb         kCentered,0
            fcz         "MC BOUNCY BALL 1.0"
            ldd         gHighScore
            jsr         Print
            fcb         kPadZero,8,3
            fcz         "HIGH SCORE : \5"
            jsr         Print
            fcb         kCentered,6,kSolidWall
            fcb         " WALLS         "
            fcb         kScoreTile
            fcz         " SCORE     "
            jsr         Print
            fcb         kCentered,7,kBreakAway
            fcb         " BREAK AWAY    "
            fcb         kExitTile
            fcz         " EXIT      "
            jsr         Print
            fcb         kCentered,8,kSlowTile
            fcb         " SLOW DOWN     "
            fcb         kDeathTrap
            fcz         " DEATH TRAP"
            jsr         Print
            fcb         1,11
            fcc         "COLLECT ENOUGH SCORE TILES TO   "
            fcc         "COMPLETE EACH LEVEL. AVOID DEATH"
            fcc         "TRAPS AND FIND THE EXIT.  FASTER"
            fcz         "TIME WILL IMPROVE YOUR SCORE.   "
            jsr         ScreenFlush
            clra                                ; initilaize tick counter to zero
            clrb
            std         gTimer
infoIdle    jsr         GetKey                  ; read keyboard
            bne         NewGame                 ; go start game if any key pressed
            ldx         gTimer                  ; read tick counter
            cpx         #11*60                  ; have 11 seconds ellapsed?
            blo         infoIdle                ; loop if no
hopAttract  jmp         Attract                 ; run Attract mode again


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Start a New Game
*
NewGame     jsr         ClrScreen                 ; clear page 0 buffer to black
            ldx         #gLevFlags                ; initialize level status..
            ldd         #kMaxLives*256+kNumLevels ; ..entries for all levels..
            jsr         MemFill                   ; ..with number of lives (3)
            clra                                  ; ACCD = 0
            std         gGameScore                ; reset game score
            stab        gGameOver                 ; clear Game Over flag
            stab        gLevelNum                 ; first level selected
            stab        gGameState                ; set initial game state for level selection
            stab        gCheat                    ; cheat mode disabled


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Run the Game Loop
*
GameLoop    ldab        gGameOver               ; if Game Over flag is set then..
            bne         hopAttract              ; ..exit game loop and enter Attract mode
            ldaa        gGameState              ; get current game state
            bmi         startLevel              ; branch if RETRY LEVEL
            bne         inPlay                  ; branch if not IDLE
            jsr         LevelSelect             ; present the Level Selection screen
            tstb                                ; level selected or BREAK key?
            bmi         hopAttract              ; back to Attract mode if BREAK key
            stab        gLevelNum               ; store selection as the current level
            ldx         #gLevFlags              ; point X at the level status..
            abx                                 ; ..entry for the current level
            ldab        ,x                      ; get level status
            andb        #kMaxLives              ; setup the number of lives..
            stab        gNumLives               ; ..remaining for this level

startLevel  jsr         ClrScreen               ; clear screen to black
            jsr         ScreenFlush             ; display the cleared screen
            jsr         PrepLevel               ; prepare level for play (expand map data, etc..)
            ldaa        gNumLives               ; are we starting the level..
            cmpa        #kMaxLives              ; ..with all lives remaining?
            blo         beginPlay               ; skip next if no
            jsr         LevShow                 ; show fast scrolling preview of level map
            beq         GameLoop                ; return to Level Selection screen if BREAK was pressed
beginPlay   jsr         Delay12
            ldx         #STATLINE+32-3          ; show the indicator for the..
            jsr         DrawLives               ; ..number of lives remaining
            ldd         #kMinScroll             ; set scroll postion to the far..
            std         gScrollPos              ; ..left end of the level map
            decb                                ; set vertical ball position..
            stab        gBallPos                ; ..to center of the screen
            jsr         GetMapTile              ; set gCurrTile global to point at tile containing the ball
            clra
            clrb
            std         gTimer8                 ; reset amount of time played
            inca                                ; change the game state..
            staa        gGameState              ; ..to RUNNING

inPlay      deca                                ; if current state is RUNNING..
            beq         runFrame                ; ..then go process next frame
            deca                                ; if current state is DIED then..
            beq         deceased                ; ..go end the level
            jsr         LevComplete             ; player has completed the level
            clr         gGameState              ; reset state to IDLE
            bra         GameLoop                ; continue the loop (prompt for new level)

deceased    deca                                ; change state..
            staa        gGameState              ; ..to RETRY LEVEL
            dec         gNumLives               ; decrement lives remaining
            ldab        gLevelNum               ; point X at the..
            ldx         #gLevFlags              ; ..level status entry..
            abx                                 ; ..for current level
            ldaa        ,x                      ; get level status
            anda        #$80                    ; keep only the "completed" flag
            oraa        gNumLives               ; combine with lives remaining
            staa        ,x                      ; update level status with new lives remaining
            ldab        #100                    ; delay for 1.6 seconds..
            jsr         DelayTicks              ; ..before erasing the screen
            jsr         LevelDeath              ; show screen to display cause of death
hopLoop1    bra         GameLoop                ; continue the loop

* Process a frame during play
runFrame    clr         gGameState              ; temporarily change state to IDLE
            jsr         GetKey                  ; read the keyboard
            cmpa        #3                      ; was BREAK key pressed?
            beq         hopLoop1                ; continue loop in IDLE state if yes
            inc         gGameState              ; restore state to RUNNING

* Horizontal movement
            ldaa        gScrollPace             ; is now the time for scrolling?
            bpl         renderFrame             ; branch if no
            neg         gScrollPace             ; setup the next time for scrolling
            ldaa        gInertia                ; get current scrolling inertia
            clr         gInertia                ; assume no scrolling for this frame
            eora        #1                      ; inverted intertia flag
            asla                                ; multiply by 2
            staa        throttle+1              ; throttle scrolling pace if not previously in motion
            ldx         gScrollPos              ; X = current horizontal position
            pshx                                ; save pre-scroll position on the stack
            inx                                 ; new postion for a rightward scroll
            ldd         #$DF01                  ; ACCA: keyboard rollover mask,  ACCB: direction flag = RIGHT
            cmpa        KEYTABLE+6              ; is the right directional key pressed?
            beq         scroll                  ; branch if yes
            ldaa        #$FB
            cmpa        KEYTABLE+3              ; other right directional key pressed?
            beq         scroll                  ; branch if yes
            pulx                                ; pull pre-scroll position from stack
            ldaa        #$DF
            cmpa        KEYTABLE+4              ; is the left directional key pressed?
            beq         scrollLeft              ; branch if yes
            ldaa        #$FE
            cmpa        KEYTABLE+1              ; other left directional key pressed?
            bne         renderFrame             ; branch if no
scrollLeft  clrb                                ; direction flag = LEFT
            pshx                                ; save pre-scroll position on the stack
            dex                                 ; new postion for a leftward scroll

scroll      stx         gScrollPos              ; set the new scroll position
            pshb                                ; save direction flag on stack
            inc         gNeedRender             ; rendering is required after a scroll
            inc         gInertia                ; set scroll inertia state to in-motion
throttle    ldaa        #*                      ; add throttling time..
            adda        gScrollPace             ; ..to the pacing timer if..
            staa        gScrollPace             ; ..not previously in motion
            jsr         GetMapTile              ; get tile at ball's new position
            pula                                ; pull direction flag from stack
            pulx                                ; pull pre-scroll postion from stack
            cmpb        #kOpenSpace             ; is the new position empty space?
            beq         renderFrame             ; branch if yes
            stx         gScrollPos              ; revert to pre-scroll position because of collision
            cmpa        gPixAlign               ; if pixel alignment needs changing to make the ball..
            bne         newAlign                ; ..adjacent to the barrier then go update and render
            dec         gNeedRender             ; rendering not needed since no scroll or alignment change
newAlign    staa        gPixAlign               ; update pixel alignment if changed


* Render this frame if required
renderFrame ldaa        gNeedRender             ; is rendering required for this frame?         
            beq         drawStatus              ; branch if no
            jsr         RenderMap               ; render visible portion of map (returns page ptr in X)
            ldaa        #%00001000              ; data for upper-left pixel in cell
            ldab        gBallPos                ; get vertical position of the ball
            lsrb                                ; divide by 2 to get cell row
            bcc         alignPix                ; branch if ball is in upper half of cell
            ldaa        #%00000010              ; data for lower-left pixel in cell
alignPix    tst         gPixAlign               ; check the current pixel alignment
            beq         pixColor                ; branch if left-aligned
            lsra                                ; adjust pixel selection for right-alignment
pixColor    oraa        #%10010000              ; combine pixel selection with SG4/color bits
            psha                                ; save SG4 character on stack
            ldaa        #36                     ; multiply cell row number by the width..
            mul                                 ; ..of one row in the page buffer (36)
            addd        pag.buffer,x            ; calculate base address of row in page buffer
            std         gTfrTemp                ; start a transfer of D to X
            sei                                 ; mask interrupts to prevent premature blitting
            inc         pag.queued,x            ; queue the page for blitting
            ldab        pag.hStart,x            ; get cell offset for scroll position
            ldx         gTfrTemp                ; finish transfer of row address into X
            addb        #16                     ; add cell offset plus half the screen..
            abx                                 ; ..width to the row address
            pula                                ; pull SG4 character from stack
            staa        ,x                      ; render cell containing the ball
            cli                                 ; unmask interrupts
            clr         gNeedRender             ; clear the rendering required flag


* Render the status line without any buffering.
drawStatus  ldd         gLevelTime              ; get time allowed for this level
            subd        gTimer8                 ; subtract time used so far
            bhi         printTime               ; branch if more time remaining
            clra                                ; time remaining = 0
            clrb
printTime   std         gTimeLeft               ; store new time remaining value
            ldx         #STATLINE               ; set cursor position to start..
            stx         gCursorPtr              ; ..of status line in video RAM
            jsr         Print                   ; print time remaining
            fcb         kPadZero,kCurrentPos
            fcz         "TIME \5  "
            clra                                ; get number of score tiles hit (zero-extended)
            ldab        gHits
            cmpb        gHitsShown              ; has number changed since last time?
            beq         checkTime               ; branch if not changed
            stab        gHitsShown              ; update number of hits displayed
            subb        gMinHits                ; subtract number of hits required to complete level
            bcs         printGoal               ; branch if more hits required
            clrb                                ; goal = 0
printGoal   negb                                ; absolute value for goal
            jsr         Print                   ; print goal remaining
            fcb         kPadSpace,kCurrentPos   
            fcz         " GOAL \# "
            jsr         GoalPercent             ; calculate percentage of total score tiles hit
            jsr         Print                   ; print the percentage
            fcb         kPadSpace,kCurrentPos
            fcz         " \3% "

* Check for time expired
checkTime   ldd         gTimeLeft               ; any time remaining?
            bne         checkHorz               ; branch if yes
            jsr         expired                 ; end the level due to no time remaining

* Check for horizontal collision with a Death Trap
checkHorz   ldx         gCurrTile               ; get tile at the ball's..
            ldab        ,x                      ; ..current position
            subb        #kDeathTrap             ; death trap?
            orab        gCheat                  ; cheat mode?
            beq         hDeath                  ; branch on death trap collision if cheat mode disabled

* Vertical movement of the ball
            ldab        gBallSpeed              ; current speed throttle for the ball
            ldaa        gBallPace               ; is the ball moving in this frame?
            bpl         hopLoop2                ; branch if no
            stab        gBallPace               ; update time for next movement based on current speed
            ldaa        gBallPos                ; current vertical position
            adda        gBallDir                ; increment/decrement
            staa        gBallPos                ; store new position
            inc         gNeedRender             ; rendering required following movement of the ball

* Detect collisions on vertical movement
            bsr         GetMapTile              ; get tile at ball's new position
            fcb         SKP2                    ; skip next instruction
hDeath      ldab        #kDeathTrap             ; select death trap for a horizontal collision
            bitb        #$0F                    ; is tile some kind of obstacle?
            beq         hopLoop2                ; branch if no (open space or hidden wall)
            tba                                 ; copy obstacle kind to ACCA
            subb        #$8F                    ; convert the obstacle kind..
            lsrb                                ; ..to an offset into the..
            lsrb                                ; ..the collision dispatch..
            lsrb                                ; ..table
            ldx         #ObsTable               ; point X at entry in dispatch table..
            abx                                 ; ..for the obstacle kind hit
            jsr         ,x                      ; call collision processor
hopLoop2    jmp         GameLoop                ; continue the game loop


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Retrieve data from tile map for position specified by gScrollPos and gBallPos
*
GetMapTile  ldd         gScrollPos              ; horizontal position of cell containing the ball
            lsrd                                ; divide by 4 to get..
            lsrd                                ; ..the tile position
            std         gCurrTile               ; store horizontal offset for tile address
            ldab        gBallPos                ; get vertical position of the ball in pixels
            bmi         outOfBounds             ; branch if ball has exited top of screen
            cmpb        #30                     ; has the ball exited bottom of screen?
            bhs         outOfBounds             ; branch if yes
            lsrb                                ; divide pixel pos by 2 to get cell row
            ldaa        #kMapWidth/2            ; multiply row number by half..
            mul                                 ; ..the width of the level map
            asld                                ; now double it for full map width
            addd        #LevelMap               ; get base address of map row
            addd        gCurrTile               ; add horizontal offset to get the tile address
            std         gCurrTile               ; store address of the tile
            ldx         gCurrTile               ; point X at map entry for the tile
            ldab        ,x                      ; return contents of the map..
            rts                                 ; ..for the tile containing the ball

outOfBounds ldab        #kOffWorld              ; return special obstacle kind which..
            ldx         #outOfBounds+1          ; ..indicates the ball has exited the..
            stx         gCurrTile               ; ..the play field.  Also set gCurrTile to..
            rts                                 ; ..point at a byte containing that value.


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Process collisions between the ball and obstacles in the map
*
ObsTable    bra         scoreHit                ; kScoreTile  ($8x)
            bra         slowDown                ; kSlowTile   ($9x)
            bra         deathTrap               ; kWaterTrap  ($Ax)
            bra         deathTrap               ; kDeathTrap  ($Bx)
            bra         wallBounce              ; kSolidWall  ($Cx)
            bra         outside                 ; kOffWorld   ($Dx)
            bra         wayOut                  ; kExitTile   ($Ex)
            ldx         #26                     ; kBreakAway  ($Fx)
            bra         clearTile

scoreHit    inc         gHits                   ; increment number of score hits so far
            ldx         #21                     ; select sound pitch for score hit

clearTile   bsr         bounce                  ; bounce off the obstacle and emit sound
            ldx         gCurrTile               ; point X at tile entry in map
            ldaa        #kOpenSpace             ; change the map entry..
            staa        ,x                      ; ..to open space
            rts

slowDown    bsr         wallBounce              ; bounce the same as a solid wall
            ldd         #$0909                  ; change ball speed to slow and set the..
            std         gBallSpeed              ; ..timeout for resumption of normal speed
            rts

wayOut      ldab        gHits                   ; have the minimum number of score..
            cmpb        gMinHits                ; ..tiles been hit for this level?
            blo         denyExit                ; branch if no
            ldaa        #kDoneState             ; change state of the game loop..
            staa        gGameState              ; ..to LEVEL COMPLETED
            rts

denyExit    ldx         #261                    ; select sound pitch for exit denied
            bra         bounce                  ; bounce off the exit and emit sound

wallBounce  ldx         #11                     ; select sound pitch for bounce off wall above
            ldaa        gBallDir                ; get current vertical direction of travel
            bmi         bounce                  ; branch if upward
            ldx         #16                     ; select sound pitch for bounce off a wall below
bounce      bsr         PlayTone                ; play selected bounce tone
            ldd         gBallPos                ; ACCA = current vertical position, ACCB = direction of travel
            negb                                ; reverse direction of travel
            aba                                 ; update position according to new direction
            std         gBallPos                ; store new position and direction of travel
            ldd         #$0100                  ; set normal ball speed and clear the..
            std         gBallSpeed              ; ..timeout counter used during slow speed
            rts

outside     ldab        gCheat                  ; if cheat mode enabled..
            bne         wallBounce              ; ..then treat it as a wall
            bsr         fatality                ; specify cause of death for..
            fcz         "BOUNCED OFF THE WORLD" ; ..leaving the play field

deathTrap   ldab        gCheat                  ; if cheat mode enabled..
            bne         wallBounce              ; ..then treat it as a wall
            cmpa        #kWaterTrap             ; is this a water level?
            beq         waterTrap               ; branch if yes
            bsr         fatality                ; specify cause of death..
            fcz         "AVOID DEATH TRAPS"     ; ..for a death trap

waterTrap   bsr         fatality                ; specify cause of death..
            fcz         "STAY OUT OF THE WATER" ; ..for a water hazard

expired     bsr         fatality                ; specify cause of death..
            fcz         "OUT OF TIME"           ; ..for ran out of time

fatality    pulx                                ; point X at string for cause of death
            stx         gObitStr                ; store for later display
            inc         gGameState              ; change game state to DIED
            ldx         #449                    ; select sound pitch for level death

* Play momentary tone for Bounces, Deaths and Score Tallys
PlayTone    bsr         WaitBlit                ; wait for queued frames to be displayed
            ldd         #$4080                  ; ACCA = duration loop counter = 64, ACCB = sound bit = '1'
tone010     bsr         pitchDelay              ; set sound = '1' and run pitch delay
            clrb                                ; ACCB = sound bit = '0'
            bsr         pitchDelay              ; set sound = '0' and run pitch delay
            deca                                ; decrement duration loop counter
            bne         tone010                 ; loop until duration expires
            rts                                 ; return

pitchDelay  stab        VDGSND                  ; update sound control bit
            pshx                                ; preserve pitch delay on stack
tone020     ldab        #$80        2 \         ; ACCB = sound bit = '1'
            dex                     3  | 8      ; decrement pitch loop counter
            bne         tone020     3 /         ; loop until pitch counter expires
            pulx                                ; restore pitch delay to X
            rts                                 ; return


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Wait for ISR to display queued buffers
*
WaitBlit    ldaa        gPage0+pag.queued       ; combine queued state of page 0..
            oraa        gPage1+pag.queued       ; ..with the state of page 1..
            bne         WaitBlit                ; and loop if either set
            rts                                 ; return


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Level Selection Screen
*
LevelSelect ldaa        gLevelNum               ; current level selection
            ldab        #$FF                    ; initial color for box selection
            pshd                                ; allocate variables on stack

** print static text into the page buffer
            jsr         ClrScreen
            jsr         Print
            fcb         kCurrentPos
            fcz         "LEVEL: "
            jsr         Print
            fcb         1,14,$CF
            fcz         " = OPEN"
            jsr         Print
            fcb         1,15,$8F
            fcz         " = COMPLETED"
            jsr         Print
            fcb         21,14
            fcz         "USE KEYS:"
            ldx         gCursorPtr
            ldab        #32-6
            abx
            ldd         #'<>'+$4040
            staa        ,x
            stab        2,x

** print selected level name
updtSelect  ldd         #$0700                  ; position the cursor for..
            jsr         Locate                  ; ..printing the level name
            tsx                                 ; point X at stack variables
            ldab        ,x                      ; current level selection
            jsr         LevelPtrB               ; point X at selected level record
            ldaa        15,x                    ; push byte following the..
            psha                                ; ..level name onto the stack
            clr         15,x                    ; insert NULL terminator following name
            jsr         OutLoop                 ; output level name to page buffer
            pula                                ; restore byte following..
            staa        ,x                      ; ..the level name

** draw boxes representing each level
            ldab        #kNumLevels-1           ; index of last level
boxLoop     ldaa        #$CF                    ; white color for "level open"
            ldx         #gLevFlags              ; point X at level status.. 
            abx                                 ; ..flag for current box
            tst         ,x                      ; level completed?
            bpl         isOpen                  ; branch if no
            ldaa        #$8F                    ; green color for "level complete"
isOpen      bsr         levBox                  ; draw colored box
            decb                                ; decrement level index
            bpl         boxLoop                 ; loop until all boxes drawn

** rotate color of box for current selection
selIdle     clra
            clrb                                ; clear the..
            std         gTimer                  ; ..ticks timer
            tsx                                 ; point X at stacked variables
            ldaa        1,x                     ; current color for box selection
            adda        #16                     ; next color
            bmi         nxtSelColor             ; skip next if not beyond last color
            ldaa        #$8F                    ; back to first color
nxtSelColor staa        1,x                     ; update current color on stack
            ldab        ,x                      ; selected level number
            bsr         levBox                  ; draw box for current selection
            jsr         ScreenFlush             ; flush page buffer to screen

** wait for key press or idle timeout
waitSelEvt  jsr         GetKey                  ; read keyboard
            bne         selKey                  ; branch if key pressed
            ldx         gTimer                  ; get ticks timer
            cpx         #7                      ; idle time expired?
            blo         waitSelEvt              ; loop if no
            bra         selIdle                 ; update box color and try again

** process selection keys
selKey      tsx                                 ; point X at stacked variables
            ldab        #1                      ; direction = increment
            cmpa        #'.'                    ; right key?
            beq         selMove                 ; branch if yes
            cmpa        #'S'
            beq         selMove
            negb                                ; direction = decrement (also Quit flag)
            cmpa        #','                    ; left key?
            beq         selMove                 ; branch if yes
            cmpa        #'A'
            bne         chkEnter                ; branch if not a left/right key
selMove     addb        ,x                      ; get new selection
            bpl         selRange                ; branch if not negative
            ldab        #kNumLevels-1           ; roll-under to last level
selRange    cmpb        #kNumLevels             ; is new selection in range?
            blo         selSet                  ; branch if yes
            clrb                                ; roll-over to first level
selSet      stab        ,x                      ; set new selection
            jmp         updtSelect              ; update display for new selection

chkEnter    cmpa        #$0D                    ; Enter Key?
            bne         chkQuit                 ; branch if no
            ldab        ,x                      ; return with selected level in ACCB
            pulx                                ; pop variables off tha stack
            rts

chkQuit     cmpa        #'Q'                    ; Quit key?
            bne         waitSelEvt              ; loop if no
            pulx                                ; pop variables off tha stack
            rts                                 ; return with negative value in ACCB

** draw box for level in ACCB
levBox      pshb                                ; push level index
            psha                                ; push SG4 color
            tba                                 ; copy level index to ACCA
            anda        #$03                    ; horizontal = (index MOD 4) * 8 + 2
            asla
            asla
            asla
            adda        #2
            andb        #$0C                    ; vertical = index / 4 * 4 + 2
            addb        #2
            jsr         Locate                  ; set cursor position for the box
            pula                                ; pull SG4 color
            ldx         gCursorPtr              ; point X at box location in page buffer
            staa        ,x                      ; store SG4 color into..
            staa        1,x                     ; ..the four bytes of..
            staa        32,x                    ; ..video RAM comprising..
            staa        33,x                    ; ..the box
            pulb                                ; pop level index
            rts                                 ; return


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Map Decompression and Level Setup
*
PrepLevel   ldaa        #kOpenSpace             ; 'open space' tiles for map initialization
            ldx         #LevelMap               ; point X at start of map
mapInit     staa        ,x                      ; init one byte in map
            inx                                 ; increment map ptr
            cpx         #LevelMap+kMapSize      ; end of map?
            blo         mapInit                 ; loop if no

            jsr         CurrentLev              ; get address of level data indexed by ACCB
            ldd         lev.width,x             ; width of map for this level
            std         calcRowEnd+1            ; store for calculation below
            clr         gMaxHits                ; clear counter for number of score tiles in map
            ldaa        lev.min,x               ; setup the minimum score hits..
            staa        gMinHits                ; ..required for the level
            ldaa        lev.time,x              ; setup time limit for the level
            ldab        #kTimeMult
            mul
            std         gLevelTime
            ldd         #$0100+kDeathTrap       ; default trap type is Death Trap
            bita        lev.water,x             ; is the Water Hazard flag set?
            beq         trapStyle               ; branch if no
            ldab        #kWaterTrap             ; use Water Hazards instead of Death Traps
trapStyle   pshb                                ; push trap style onto the stack
            ldab        #lev.sizeof             ; point X at first byte..
            abx                                 ; ..of compressed map data
            clrb                                ; map row number = 0

rowLoop     pshb                                ; save row number on stack
            ldaa        #kMapWidth/2            ; multiply row number..
            mul                                 ; ..by half the map width..
            asld                                ; ..then double it
            addd        #LevelMap               ; get starting address of the row
            std         gTempPtr                ; temp store
calcRowEnd  addd        #*                      ; get ending address of the row
            std         rowEnd+1                ; store for comparison below

nextRun     ldaa        ,x                      ; next byte of compressed level data
            inx                                 ; increment source ptr..
            pshx                                ; ..and save on the stack
            tab                                 ; copy data byte to ACCB
            anda        #$F8                    ; ACCA = repeat count * 8
            andb        #$07                    ; ACCB = tile index
            ldx         #tilesTable             ; point at tile lookup table
            abx                                 ; point at index tile entry
            ldab        ,x                      ; get actual tile value
            bne         startRun                ; branch if not a Trap tile
            tsx                                 ; get value of trap tiles..
            ldab        3,x                     ; ..used in this level
startRun    ldx         gTempPtr                ; point X at map destination
repLoop     stab        ,x                      ; store tile in map
            inx                                 ; increment destination ptr
            cmpb        #kScoreTile             ; was it a score tile?
            bne         repNext                 ; branch if no
            inc         gMaxHits                ; update number of score tiles in the level
repNext     suba        #8                      ; decrement the repeat counter
            bcc         repLoop                 ; loop until -1
            stx         gTempPtr                ; temp store the destination ptr
rowEnd      cpx         #*                      ; have we reached end of the row?
            pulx                                ; pull source ptr into X
            bne         nextRun                 ; loop if not at end of row

            pulb                                ; pull row number
            incb                                ; increment row number
            cmpb        #kMapHeight             ; all rows complete?
            blo         rowLoop                 ; loop if no

            ins                                 ; pop trap style off the stack
            ldaa        #$FF                    ; initialize variables:
            staa        gHitsShown              ;   hits shown = -1
            clra
            staa        gSlowTime               ;   slow ball speed = FALSE
            staa        gHits                   ;   score hits = 0
            staa        gPixAlign               ;   ball pixel alignment = LEFT
            inca
            staa        gBallDir                ;   ball direction = DOWN
            staa        gBallSpeed              ;   ball speed = NORMAL
            staa        gNeedRender             ;   needs rendering flag = TRUE
            rts                                 ; return

tilesTable  fcb         kOpenSpace,kScoreTile
            fcb         kSolidWall,kHiddenWall
            fcb         kBreakAway,kExitTile
            fcb         0,kSlowTile             ; 0 means Death Trap or Water Hazard according to level setting


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Level Completed / Score Tally screen
*
LevComplete ldaa        #200                    ; pitch for note 1
cmplt010    ldab        #2                      ; duration for notes 1..3
cmplt020    psha                                ; save pitch on stack
            jsr         Sound                   ; play note
            pula                                ; pull pitch from stack
            adda        #5                      ; increment pitch for next note
            ldab        #4                      ; duration for note 4
            cmpa        #215                    ; which note?
            blo         cmplt010                ; loop if note 1..3
            beq         cmplt020                ; loop if note 4

            ldx         #gLevFlags              ; calculate address..
            ldab        gLevelNum               ; ..of the flag byte..
            abx                                 ; ..for this level
            ldab        #$80+kMaxLives          ; store "level completed" flag
            stab        ,x
            jsr         DelayTicks              ; delay for 128 ticks (2.1 seconds)
            jsr         ClrScreen               ; clear page 0 buffer to black
            jsr         Print                   ; print "LEVEL COMPLETE"
            fcb         kCentered,0
            fcz         "LEVEL COMPLETE"
            clra                                ; copy score/time remaining into code below
            ldab        gHits
            std         tileScore+1
            ldd         gTimeLeft
            std         timeScore+1
            jsr         tallyPrint
            ldab        #80                     ; delay 80 ticks (1.2 seconds)
            jsr         DelayTicks

** Add up points for the number of score tiles hit
            ldx         #tileScore+1
            ldd         ,x
            beq         addTime
            ldd         #49
            std         tallyTone+1
            ldab        #10
            jsr         TallyUp
            ldab        #45
            jsr         DelayTicks

** Add up bonus points if 100% of score tiles were hit
            jsr         GoalPercent
            cmpb        #100
            bne         addTime
            jsr         Print
            fcb         kCentered,8
            fcz         "100% BONUS"
            ldd         #43
            std         tallyTone+1
            ldd         #10
            pshd
            tsx
            jsr         TallyUp
            pulx
            ldab        #30
            jsr         DelayTicks

** Add up points for time remaining on the clock
addTime     ldx         #timeScore+1
            ldd         ,x
            beq         scoreDone
            ldd         #49
            std         tallyTone+1
            ldab        #1
            jsr         TallyUp
            ldab        #20
            jsr         DelayTicks

** Wait for a key to continue
scoreDone   jsr         Print
            fcb         kCentered,13
            fcz         "PRESS KEY TO CONTINUE"
            jsr         ScreenFlush
waitCont    jsr         GetKey
            beq         waitCont
            ldab        gLevelNum
            incb
            cmpb        #kNumLevels
            blo         setNxtLev
            clrb
setNxtLev   stab        gLevelNum

** Count number of levels that have been completed
            ldx         #gLevFlags-1
            ldd         #kNumLevels
countLevs   inx
            tst         ,x
            bpl         incomplete
            inca
incomplete  decb
            bne         countLevs
            cmpa        #kNumLevels
            beq         Finale
            rts

** Display Final Score for Completed Game
Finale      inc         gGameOver
            jsr         ClrScreen
            jsr         Print
            fcb         kCurrentPos
            fcc         "CONGRATULATIONS!!!  YOU HAVE    "
            fcc         "COMPLETED ALL TWELVE LEVELS     "
            fcz         "OF BOUNCY BALL!"
            ldd         gGameScore
            jsr         Print
            fcb         kPadSpace,1,4
            fcz         "YOUR FINAL SCORE IS \#."
            jsr         ScreenFlush

waitKey     jsr         GetKey
            cmpa        #3
            beq         ret1
            cmpa        #' '
            beq         ret1
            cmpa        #$0D
            bne         waitKey
ret1        rts

** add value of [X] * ACCB to the score using increments of 100, then 10, then 1
TallyUp     stab        gTallyInc               ; store score increment
            ldd         ,x                      ; get score multiplier
            beq         ret1                    ; branch if zero
            ldab        #100                    ; first increment by hundreds
            bsr         tallyPart
            ldab        #10                     ; next increment by tens
            bsr         tallyPart
            ldab        #1                      ; finally increment by ones
tallyPart   clra                                ; divisor MSB = 0
            std         gTallyDiv               ; store divisor
tallyLoop   ldd         ,x                      ; get score multiplier
            subd        gTallyDiv               ; subtract divisor
            blo         ret1                    ; return if divisor doesnt fit
            bne         tallyBump               ; branch if any remainder
            ldab        gTallyDiv+1             ; get divisor
            decb                                ; does divisor = 1 ?
            bne         ret1                    ; return if no
tallyBump   std         ,x                      ; store remaining score multiplier
            ldd         gTallyDiv+1             ; ACCA = gTallyDiv, ACCB = gTallyInc
            mul                                 ; compute increment value
            addd        gGameScore              ; add increment to..
            std         gGameScore              ; ..current score
            subd        gHighScore              ; new high score?
            bls         tallyShow               ; branch if no
            addd        gHighScore              ; update the..
            std         gHighScore              ; ..high score
tallyShow   pshx                                ; preserve X
            bsr         tallyPrint              ; update score display
tallyTone   ldx         #*                      ; pitch for score sound
            jsr         PlayTone                ; emit sound
            jsr         DelayOne                ; delay one tick
            pulx                                ; restore X
            bra         tallyLoop

** print current score tally to screen
tallyPrint  ldd         gHighScore
            jsr         Print
            fcb         kPadZero,9,2
            fcz         "HIGH SCORE \5"
            ldd         gGameScore
            jsr         Print
            fcb         kPadZero,9,3
            fcz         "YOUR SCORE \5"
            bsr         GoalPercent
            jsr         Print
            fcb         kPadSpace,9,5
            fcz         "HITS  \3%"
tileScore   ldd         #*
            jsr         Print
            fcb         kPadZero,kCurrentPos
            fcz         " \5"
timeScore   ldd         #*
            jsr         Print
            fcb         kPadZero,9,6
            fcz         "TIME BONUS \5"
            jmp         ScreenFlush


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Calculate percentage of score tiles that have been hit
*   percentage = gHits * 100 / gMaxHits
*
GoalPercent clra                                ; ACCD = number of score..
            ldab        gMaxHits                ; ..tiles in the level (zero-extended)
            beq         retPercnt               ; return if none
            pshd                                ; push onto stack
            ldaa        gHits                   ; multiply number..
            ldab        #100                    ; ..of tiles hit..
            mul                                 ; ..by 100
            pshd                                ; push onto stack
            ldd         #16                     ; push loop counter..
            pshb                                ; ..onto stack
            clrb                                ; clear space for..
            pshd                                ; ..result on the stack
            tsx                                 ; point X at stacked data

* stack:
*   0,x = space for 16-bit result
*   2,x = loop counter
*   3,x = gHits * 100
*   5,x = gMaxHits zero-extended to 16 bits

div16Loop   lsl         4,x                     ; shift MSB of dividend into carry
            rol         3,x                     ; shift carry and MSB of dividend, into carry
            rolb                                ; new bit of dividend now in bit 0 of ACCB
            rola
            subd        5,x                     ; does divisor fit?
            bcc         quoRot1                 ; branch if yes
            addd        5,x
            clc
            fcb         SKP1
quoRot1     sec
            rol         1,x                     ; shift carry into quotient
            rol         ,x
            dec         2,x                     ; more bits to process?
            bne         div16Loop               ; branch if yes
            puld                                ; quotient to return
            ins                                 ; pop stack
            pulx
            pulx
retPercnt   rts


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* SOUND function with pitch equivalence to CoCo Color Basic.
* Called with pitch in ACCA and duration in ACCB.
*
Sound       staa        sndPitch+1
            jsr         WaitBlit                ; wait for any queued video frames to be flushed
            clra
            asld
            asld
            std         gSoundTimer
sndCycle    clra
            bsr         sndLow
            bsr         sndHigh
            bsr         sndHigh
            clra
            bsr         sndLow
            ldd         gSoundTimer
            bne         sndCycle
            rts

sndHigh     ldaa        #$80
sndLow      staa        VDGSND
            nop
sndPitch    ldaa        #*
sndLoop     inca
            bne         sndLoop
            rts


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Death notification screen 
*
LevelDeath  jsr         ClrScreen               ; clear buffer to Black
            ldx         #gPageBuf0+512-3        ; point X at buffer location for Lives indicator
            bsr         DrawLives               ; draw the Number of Lives indicator
            ldx         gObitStr                ; get pointer to message string
            jsr         CenterPos               ; calculate horizontal cursor postion for centered string
            ldab        #6                      ; vertical cursor position = line 6
            jsr         Locate                  ; update the cursor position
            jsr         OutLoop                 ; output message string to screen buffer
            ldab        gNumLives               ; any more lives remaining?
            bne         dead010                 ; branch if yes

            jsr         Print                   ; print "GAME OVER" centered on line 0
            fcb         kCentered,0
            fcz         "GAME OVER"
            ldd         gGameScore              ; print "FINAL SCORE #####" centered on line 2
            jsr         Print
            fcb         kPadZero,9,2
            fcz         "FINAL SCORE \5"
            jsr         ScreenFlush             ; flush buffer to screen
            ldd         #$3202                  ; play 3-note final death tune
            bsr         Sound
            ldd         #$1902
            bsr         Sound
            ldd         #$010A
            bsr         Sound
            inc         gGameOver               ; set Game Over flag

dead010     jsr         Print                   ; print the continue prompt centered on line 10
            fcb         kCentered,10
            fcz         "PRESS KEY TO CONTINUE"
            jsr         ScreenFlush             ; flush buffer to screen
dead020     jsr         GetKey                  ; wait for a key press to continue
            beq         dead020
            rts


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Draw indicator for number of lives remaining.
* Called with pointer to 3 byte destination in X. 
*
DrawLives   ldd         #$8000+kMaxLives        ; ACCA = blank, ACCB = max lives
lif010      cmpb        gNumLives
            bhi         lif020
            ldaa        #$92                    ; ACCA = indicator pixel
lif020      staa        ,x                      ; store pixel or blank
            inx
            decb
            bne         lif010
            rts


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Show level preview prior to first play.
*
LevShow     ldd         #-2                     ; set initial scroll..
            std         bumpView+1              ; ..direction to leftward
            jsr         CurrentLev              ; point X at level record
            ldd         lev.width,x             ; get map width for level
            asld                                ; multiply map width..
            asld                                ; ..by tile width (4)
            std         gPreviewMax             ; set max scroll position
            std         gScrollPos              ; start preview from right side of map

viewFrame   bsr         RenderMap               ; render map into page buffer
            ldd         #257-35                 ; offset to message border
            addd        pag.buffer,x            ; add page buffer address
            addb        pag.hStart,x            ; add 1/4 tile scroll pos
            adca        #0
            std         gCursorPtr              ; set cursor position
            pshx                                ; save page ptr on stack
            ldx         #startMsg-1             ; point X at data for message box (-1)

startBox    ldaa        #1                      ; repeat count = 1
setRpt      tab                                 ; move repeat count from ACCA to ACCB
            inx                                 ; increment source data ptr
            ldaa        ,x                      ; get next data byte
            beq         viewKey                 ; branch if null terminator
            cmpa        #' '                    ; is it a repeat count?
            blo         setRpt                  ; branch if yes
rptChr      jsr         OutChar                 ; output data to page buffer
            decb                                ; decrement repeat count
            bne         rptChr                  ; loop until 0
            bra         startBox                ; loop for next byte

viewKey     jsr         GetKey                  ; read keyboard
            pulx                                ; pull page ptr into X
            bne         viewExit                ; branch if key pressed
            inc         pag.queued,x            ; queue page for display

            ldx         #1600                   ; run delay loop between frames
vwDelay     dex
            bne         vwDelay

            ldd         gScrollPos              ; get current scroll pos
bumpView    addd        #*                      ; increment/decrement by 2
            std         gScrollPos              ; set new scroll pos
            bcs         leftDir                 ; branch if scrolling leftward
            subd        gPreviewMax             ; have we reached far right side of map?
            blo         viewFrame               ; loop for next frame if no

dirChange   com         bumpView+1              ; negate the..
            neg         bumpView+2              ; ..scroll direction
            bra         viewFrame               ; loop for next frame

leftDir     subd        #kMinScroll             ; have we reached far left side of map?
            bhi         viewFrame               ; loop for next frame if no
            bra         dirChange               ; change direction

viewExit    psha                                ; save key char on stack
            jsr         ClrScreen               ; clear screen to..
            jsr         ScreenFlush             ; ..black and display
            jsr         Delay12                 ; delay for 12 ticks
            pula                                ; pull saved key char
            cmpa        #3                      ; return with CC.Z set if BREAK key pressed
            rts

** data for message box
startMsg    fcb         $DE,18,$DC,$DD,16,'^',$DA
            fcc         "PRESS KEY TO BEGIN"
            fcb         $D5,16,'^',$DB,18,$D3,$D7,0


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Render visible portion of tile map into current page buffer
*
RenderMap   ldx         #gPage0                 ; point X at page 0
            ldab        gRenderPage             ; get offset of page to render
            abx                                 ; point X at that page
            pshx                                ; save page ptr on stack
            eorb        #pag.sizeof             ; update and store offset..
            stab        gRenderPage             ; ..for the next page
waitRender  ldaa        pag.queued,x            ; wait for page to be..
            bne         waitRender              ; ..removed from blit queue
            ldab        gScrollPos+1            ; LSB of horizontal position
            andb        #$03                    ; compute and store the 1/4 tile..
            stab        pag.hStart,x            ; ..offset value
            ldd         gScrollPos              ; get horizontal position of cell containing the ball
            lsrd                                ; divide by 4 to get..
            lsrd                                ; ..the tile position
            addd        #LevelMap-5             ; calculate address of upper-left map tile to render (-1)
            std         setSrcPtr+1             ; modify code for loading address into S
            ldx         pag.buffer,x            ; point X at page buffer
            ldaa        #kMapHeight             ; setup 1st loop counter for the..
            staa        gCounter1               ; ..fifteen rows of the map
            sei                                 ; mask interrupts while using S for indexing
            sts         gTempPtr                ; save the stack ptr
setSrcPtr   lds         #*                      ; point S at source position in tile map (-1)
            bra         renderRow               ; jump into the tile rendering loop

rLoop1      ldd         gTfrTemp                ; finalize transfer of S to D
            addd        #kMapWidth-8-1          ; adjustment to point at first tile to render in next row
            sei                                 ; mask interrupts while using S for indexing
            std         gTfrTemp                ; transfer D..
            lds         gTfrTemp                ; ..back into S
renderRow   ldaa        #3                      ; setup 2nd loop counter for 9 tiles..
            staa        gCounter2               ; ..(3 groups of 3) rendered per row.
rLoop2      pula                                ; get next tile from the level map
            tab                                 ; duplicate it in ACCA
            std         ,x                      ; render the tile into four consecutive..
            std         2,x                     ; ..bytes of the page buffer
            pula                                ; render 2nd tile of the group
            tab
            std         4,x
            std         6,x
            pula                                ; render 3rd tile of the group
            tab
            std         8,x
            std         10,x
            ldab        #12                     ; increment X to point at next group..
            abx                                 ; ..of three tiles in the page buffer
            dec         gCounter2               ; decrement inner loop counter
            bne         rLoop2                  ; loop if more to do on this row
            sts         gTfrTemp                ; begin transfer of S to D for adjustment
            lds         gTempPtr                ; restore stack pointer
            cli                                 ; allow for interrupt servicing between rows
            dec         gCounter1               ; decrement outer loop counter
            bne         rLoop1                  ; loop if more rows to render
            pulx                                ; return with pointer..
            rts                                 ; ..to rendered page in X


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Return ASCII character of latest keypress in ACCA  (0 if none)
*
GetKey      stx         keySavX+1               ; save X for later
            ldx         KEYVEC                  ; get ROM vector to KEYIN routine
            pshx                                ; push vector onto stack
keySavX     ldx         #*                      ; restore X
            rts                                 ; jump to KEYIN


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*   DelayOne :  delay until the next increment of gTimer.
*    Delay12 :  delay until gTimer increments 12 times.
* DelayTicks :  delay until gTimer increments ACCB times.
*
*   X is clobbered.
*
Delay12     ldab        #12
            fcb         SKP2
DelayOne    ldab        #1
DelayTicks  ldx         gTimer
            abx
delayLoop   cpx         gTimer
            bne         delayLoop
            rts


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Blit the first 512 bytes of page 0 buffer to video RAM.
*
ScreenFlush clrb
            stab        gPage1+pag.queued       ; cancel anything queued in page 1
            clra
            std         gRenderPage             ; reset render/blit page numbers
            tpa                                 ; save current IRQ..
            psha                                ; ..mask on the stack
            bita        #$10                    ; is IRQ mask set?
            bne         flush010                ; skip next if yes
            bsr         DelayOne                ; wait for next frame sync
flush010    ldx         #VIDRAM                 ; X = video RAM destination ptr
            sei                                 ; mask IRQs
            sts         gTempPtr                ; save stack ptr
            lds         #gPageBuf0-1            ; S = page buffer source ptr
flush020    pula                                ; get byte from page buffer
            staa        ,x                      ; store in video RAM
            inx                                 ; increment destination ptr
            cpx         #VIDRAM+512             ; end of video RAM?
            blo         flush020                ; loop if no
            lds         gTempPtr                ; restore stack ptr
            pula                                ; restore the..
            tap                                 ; ..old IRQ mask
            rts


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Clear page 0 buffer to black and move cursor to upper-left
*
ClrScreen   ldx         #gPage0                 ; page 0 always used for non-play buffer
            clr         pag.queued,x            ; make sure it isn't queued for display
            ldx         pag.buffer,x            ; point X at the buffer
            ldd         #$8000                  ; ACCA = fill (black), ACCB = count (256)
            bsr         MemFill                 ; clear 1st half
            bsr         MemFill                 ; clear 2nd half
            clra                                ; ACCD = 0

    **** FALL THRU ****

* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Move cursor to X,Y location (ACCA,ACCB) in the page 0 buffer
*
Locate      anda        #31                     ; limit X value to 0..31
            andb        #15                     ; limit Y value to 0..15
            staa        locX+2                  ; store X value for calculation below
            ldaa        #32                     ; multiply Y value by 32
            mul
            addd        #gPageBuf0              ; get row start address
locX        addd        #0                      ; add X value
            std         gCursorPtr              ; store absolute cursor address
            rts



* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Calculate offset for printing string centered horizontally.
* Called with pointer to null-terminated string in X.
* Returns with horizontal offset in ACCA.  X and ACCB are preserved.
*
CenterPos   pshx                                ; save string ptr on stack
            ldaa        #-1                     ; initialize length
cntrLoop    inca                                ; increment length
            inx                                 ; increment string ptr
            tst         ,x                      ; end of string?
            bne         cntrLoop                ; loop if no
            lsra                                ; compute horizontal position..
            nega                                ; ..for start of string as:
            adda        #16                     ;    16 - (length / 2)
            pulx                                ; restore string ptr to X
            rts                                 ; return


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* MemZero:  X = ptr, ACCB = count
* MemFill:  X = ptr, ACCB = count, ACCA = fill byte
*
MemZero     clra
MemFill     staa        ,x
            inx
            decb
            bne         MemFill
            rts


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Print a string with optional numeric parameter (passed in ACCD).
*
Print       pulx                                ; point X at data
            tst         ,x                      ; test flags
            beq         prnt030                 ; branch if no parameter or location
            bpl         prnt010                 ; branch if no parameter
            bsr         num2string              ; convert integer to string
            inx                                 ; point X at the location info
prnt010     ldd         ,x                      ; get location
            deca                                ; test location flag
            bmi         prnt030                 ; branch if no change
            inx                                 ; point X at line number
            cmpa        #32                     ; test horizontal position
            blo         prnt020                 ; branch if not "centered"
            bsr         CenterPos               ; calculate centering offset
prnt020     bsr         Locate                  ; update cursor location
prnt030     inx                                 ; point X at next char to print
            ldaa        ,x                      ; get char
            bne         prnt040                 ; branch if not NULL
            jmp         1,x                     ; return to caller
prnt040     cmpa        #'\'                    ; if char is a backslash then..
            beq         prntVal                 ; ..go print numeric parameter
            bsr         OutChar                 ; output char to page buffer
            bra         prnt030                 ; loop for next char

prntVal     inx                                 ; get justificaction char..
            ldab        ,x                      ; ..from string
            cmpb        #'2'                    ; justification of 2 or more?
            bhs         valJust                 ; branch if yes
            ldab        numStrSig               ; use number of significant digits
            addb        #'0'                    ; binary to ASCII
valJust     negb                                ; compute number of numeric..
            addb        #'5'                    ; ..characters to be skipped
            pshx                                ; save string ptr on the stack
            ldx         #numStrBuf              ; point X at numeric string buffer
            abx                                 ; skip past unwanted digits
            bsr         OutLoop                 ; output digits to page buffer
            pulx                                ; restore string ptr to X
            bra         prnt030                 ; loop for next char


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Output characters pointed to by X until NULL terminator encountered.
*
OutLoop     ldaa        ,x                      ; get character from source
            beq         outcRet                 ; return if NULL terminator
            bsr         OutChar                 ; output character to screen/page buffer
            inx                                 ; increment source ptr
            bra         OutLoop                 ; loop for next char


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Output character in ACCA.
*
OutChar     pshx                                ; preserve X
            ldx         gCursorPtr              ; X = cursor location
            cmpa        #'^'                    ; special char to advance cursor only?
            beq         outc011                 ; branch if yes
            tsta                                ; graphics char?
            bmi         outc010                 ; branch if yes
            cmpa        #$20                    ; inverted char?
            blo         outc007                 ; branch if yes
            cmpa        #$40                    ; number / punctuation?
            blo         outc010                 ; branch if yes
            cmpa        #$60                    ; upper case alpha?
            blo         outc005                 ; branch if yes
            anda        #$1F                    ; change lower case ASCII to upper case VDG code
outc005     eora        #$40
            fcb         SKP2                    ; skip next
outc007     adda        #$60                    ; invert color
outc010     staa        ,x                      ; store VDG char at cursor location
outc011     inx                                 ; increment and store..
            stx         gCursorPtr              ; ..new cursor location 
            pulx                                ; restore X
outcRet     rts


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Conversion of integer parameter to string for the Print routine.
*
num2string  std         numStrVal               ; store value
            ldaa        ,x                      ; get flags byte
            anda        #$01                    ; keep the "zero-padded" flag
            staa        numStrZer
            clr         numStrPos               ; initialize output buffer position
            ldab        #5                      ; setup significant digits count
            stab        numStrSig
            ldd         #10000                  ; divisor = 10,000
            bsr         dig2strD                ; extract ten thousands digit
            ldd         #1000                   ; divisor = 1,000
            bsr         dig2strD                ; extract thousands digit
            ldab        #100                    ; divisor = 100
            bsr         dig2strB                ; extract hundreds digit
            ldab        #10                     ; divisor = 10
            bsr         dig2strB                ; extract tens digit
            ldab        #1                      ; divisor = 1
            stab        numStrZer               ; set "zero-padded" flag for ones digit
dig2strB    clra                                ; zero-extend an 8-bit divisor
dig2strD    std         numStrDiv               ; store divisor
            ldaa        #'0'-1                  ; initialize ASCII digit
            fcb         SKP2                    ; skip next
d2st010     staa        numStrZer               ; 
            inca                                ; increment ASCII digit
            psha                                ; save on stack
            ldd         numStrVal               ; get remaining value
            subd        numStrDiv               ; subtract divisor
            bcc         d2st020                 ; branch if divisor fits
            addd        numStrDiv               ; undo subtraction
d2st020     std         numStrVal               ; store remaining value
            pula                                ; pull ASCII digit
            bcc         d2st010                 ; loop if divisor fit
            ldab        numStrZer               ; test "zero-padded" flag
            bne         d2st030                 ; branch if set
            ldaa        #' '                    ; change character to space
            dec         numStrSig               ; decrement number of significant digits
d2st030     pshx
            ldx         #numStrBuf
            ldab        numStrPos               ; get output buffer position
            abx
            staa        ,x
            incb
            stab        numStrPos
            pulx
            rts


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*  Service routine for the Timer Output Compare interrupt
*
ServiceTOC  ldd         TOCR                    ; get this IRQ time
            addd        gVidTiming              ; add the timing interval
            bita        TCSR                    ; clear Output Compare flag
            std         TOCR                    ; set next IRQ time

** decrement duration timer for the Sound routine
            ldx         gSoundTimer             ; is Sound playing?
            beq         tickUpdt                ; branch if no
            dex                                 ; update the sound..
            stx         gSoundTimer             ; ..duration timer
            rti                                 ; exit ISR

** increment Tick timers
tickUpdt    ldd         gTimer                  ; get current ticks count
            addd        #1                      ; increment by one
            std         gTimer                  ; update global
            andb        #$07                    ; multiple of 8? 
            bne         hPace                   ; branch if no
            ldx         gTimer8                 ; update the "8 ticks" timer
            inx
            stx         gTimer8

** update game pacing globals
hPace       ldaa        gScrollPace             ; test global for horizontal scroll pace
            bmi         vPace                   ; branch if already set (negative)
            dec         gScrollPace             ; decrement scroll pace 

vPace       ldaa        gBallPace               ; test global for vertical ball movement pace
            bmi         blitter                 ; branch if already set (negative)
            dec         gBallPace               ; decrement ball pace
            bpl         blitter                 ; branch if still not set (positive)

** check if it is time to resume normal ball speed
            ldd         gBallSpeed              ; get speed throttle and slow speed timeout
            tstb                                ; slow speed in effect?
            beq         blitter                 ; branch if no
            decb                                ; decrement timeout
            bne         speedUpdt               ; branch if not time to resume normal speed
            ldaa        #1                      ; change throttle value to normal speed
speedUpdt   std         gBallSpeed              ; store new throttle and timeout


** blit queued page buffer to video RAM
blitter     ldx         #gPage0                 ; point at Page 0 info
            ldab        gBlitPage               ; get offset of page to blit
            abx                                 ; update X to point at current page
            ldaa        pag.queued,x            ; is page queued for dispaly?
            beq         irqRet                  ; branch if no
            clr         pag.queued,x            ; de-queue
            eorb        #pag.sizeof             ; toggle page offset
            stab        gBlitPage               ; set next page to blit
            ldab        pag.hStart,x            ; get 1/4 tile horizontal offset for the page
            ldx         pag.buffer,x            ; point X at page buffer
            abx                                 ; add 1/4 tile offset to X
            sts         blitSaveS+1             ; save stack ptr for later restoration
            txs                                 ; point S at first byte to blit in page buffer
            ldx         #VIDRAM                 ; point X at first byte of video RAM
rowBlit     puld                                ; blit one row of the display (32 bytes)
            std         ,x
            puld
            std         2,x
            puld
            std         4,x
            puld
            std         6,x
            puld
            std         8,x
            puld
            std         10,x
            puld
            std         12,x
            puld
            std         14,x
            puld
            std         16,x
            puld
            std         18,x
            puld
            std         20,x
            puld
            std         22,x
            puld
            std         24,x
            puld
            std         26,x
            puld
            std         28,x
            puld
            std         30,x
            ins                                 ; increment S by width of extra tile alignment (4 bytes)
            ins
            ins
            ins
            ldab        #32                     ; increment X by width of one video row (32 bytes)
            abx
            cpx         #VIDRAM+(32*kMapHeight) ; completed all rows?
            blo         rowBlit                 ; loop if more
blitSaveS   lds         #*                      ; restore stack ptr
irqRet      rti                                 ; exit ISR


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Get pointer to current Level Record
*
CurrentLev  ldab        gLevelNum               ; ACCB = current level number
LevelPtrB   ldx         #LevelsData             ; point X at first level record (title graphics)
            addb        #2                      ; index adjustment
getLev010   decb                                ; decrement index
            beq         getLev020               ; branch if pointing at requested level
            pshb                                ; save index on stack
            ldd         ,x                      ; get total size of this level record
            pshx                                ; push level ptr onto stack
            tsx                                 ; point X at the level ptr
            addd        ,x                      ; add record size to level ptr
            std         ,x                      ; store ptr to next level on the stack
            pulx                                ; pull next level ptr into X
            pulb                                ; ACCB = index
            bra         getLev010               ; loop

getLev020   inx                                 ; return with X pointing at first
            inx                                 ; ..field of the level record (name)
            rts


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Import The Level Records
*
LevelsData  equ         *
            .copy       "levels/title.lev"      ; level containing title graphics

            .copy       "levels/mellow.lev"
            .copy       "levels/forest.lev"
            .copy       "levels/blocks.lev"
            .copy       "levels/pipes.lev"
            .copy       "levels/challenge1.lev"
            .copy       "levels/caves.lev"
            .copy       "levels/tweasers.lev"
            .copy       "levels/squareball.lev"
            .copy       "levels/challenge2.lev"
            .copy       "levels/wave.lev"
            .copy       "levels/challenge3.lev"
            .copy       "levels/hideout.lev"


* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Memory for the decompressed level map used during play begins here.
* Requires enough RAM for kMapWidth * kMapHeight bytes.
*
LevelMap    equ         *
********    rmb         kMapSize


            .end



